Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Faster entity cloning #16717

Merged
merged 46 commits into from
Dec 18, 2024
Merged

Conversation

eugineerd
Copy link
Contributor

@eugineerd eugineerd commented Dec 8, 2024

Objective

#16132 introduced entity cloning functionality, and while it works and is useful, it can be made faster. This is the promised follow-up to improve performance.

Solution

PREFACE: This is my first time writing unsafe in rust and I have only vague idea about what I'm doing. I would encourage reviewers to scrutinize unsafe parts in particular.

The solution is to clone component data to an intermediate buffer and use EntityWorldMut::insert_by_ids to insert components without additional archetype moves.

To facilitate this, EntityCloner::clone_entity now reads all components of the source entity and provides clone handlers with the ability to read component data straight from component storage using read_source_component and write to an intermediate buffer using write_target_component. ComponentId is used to check that requested type corresponds to the type available on source entity.

Reflect-based handler is a little trickier to pull of: we only have &dyn Reflect and no direct access to the underlying data. ReflectFromPtr can be used to get &dyn Reflect from concrete component data, but to write it we need to create a clone of the underlying data using Reflect. For this reason only components that have ReflectDefault or ReflectFromReflect or ReflectFromWorld can be cloned, all other components will be skipped. The good news is that this is actually only a temporary limitation: once #13432 lands we will be able to clone component without requiring one of these type datas.

This PR also introduces entity_cloning benchmark to better compare changes between the PR and main, you can see the results in the showcase section.

Testing

  • All previous tests passing
  • Added test for fast reflect clone path (temporary, will be removed after reflection-based cloning lands)
  • Ran miri

Showcase

Here's a table demonstrating the improvement:

benchmark main, avg PR, avg change, avg
many components reflect 18.505 µs 2.1351 µs -89.095%
hierarchy wide reflect* 22.778 ms 4.1875 ms -81.616%
hierarchy tall reflect* 107.24 µs 26.322 µs -77.141%
hierarchy many reflect 78.533 ms 9.7415 ms -87.596%
many components clone 1.3633 µs 758.17 ns -45.937%
hierarchy wide clone* 2.7716 ms 3.3411 ms +20.546%
hierarchy tall clone* 17.646 µs 20.190 µs +17.379%
hierarchy many clone 5.8779 ms 4.2650 ms -27.439%

*: these benchmarks have entities with only 1 component

Considerations

Once #10154 is resolved a large part of the functionality in this PR will probably become obsolete. It might still be a little bit faster than using command batching, but the complexity might not be worth it.

Migration Guide

  • &EntityCloner in component clone handlers is changed to &mut ComponentCloneCtx to better separate data.
  • Changed EntityCloneHandler from enum to struct and added convenience functions to add default clone and reflect handler more easily.

@alice-i-cecile alice-i-cecile added this to the 0.16 milestone Dec 8, 2024
@alice-i-cecile alice-i-cecile added A-ECS Entities, components, systems, and events C-Performance A change motivated by improving speed, memory usage or compile times A-Reflection Runtime information about types D-Modest A "normal" level of difficulty; suitable for simple features or challenging fixes D-Unsafe Touches with unsafe code in some way S-Needs-Review Needs reviewer attention (from anyone!) to move forward labels Dec 8, 2024
@eugineerd
Copy link
Contributor Author

Fixed tests for reflect-based cloning paths not actually checking reflect paths (oops) and clone_fast path in reflect handler not actually writing pointers to component data (which the tests should've caught). This fix didn't affect benchmark results in any noticeable way.

@alice-i-cecile
Copy link
Member

@JaySpruce can I get your review here please?

crates/bevy_ecs/src/component.rs Outdated Show resolved Hide resolved
crates/bevy_ecs/src/component.rs Outdated Show resolved Hide resolved
crates/bevy_ecs/src/component.rs Show resolved Hide resolved
@MrGVSV
Copy link
Member

MrGVSV commented Dec 17, 2024

The main difference is that Reflect-based clone handler now requires ReflectFromPtr to work, but I think this should be fine.

Thankfully this should be totally fine since the derive macro always registers it with no way to opt out :)

if self
.components
.component_id::<T>()
.is_some_and(|id| id == self.component_id)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It seems unfortunate to have to look up the component_id again when we already have it, but I guess it would be hard to pass along the safety requirements, and it's not a very expensive lookup.

This also means it can't work with dynamic components. It might be worth doing the lookup the other way, getting the ComponentDescriptor for self.component_id and checking that type_id() == Some(TypeId::of::<T>()). I don't actually know whether dynamic components based on a rust type will have a type_id(), though, so it might not matter.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This also means it can't work with dynamic components.

I think that we can probably allow to clone dynamic components by exposing some unsafe methods on ComponentCloneCtx to get source component Ptr and to copy from provided Ptr to target. This would let someone who created dynamic component to register their own component clone handler.

let source_type_id = self
.components
.get_info(self.component_id)
.unwrap()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These unwrap()s should probably be expect()s so that we can give more helpful error messages.

}
let source_type_id = self
.components
.get_info(self.component_id)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This could use the component_info variable from a few lines later to save one lookup.

For that matter, it might be worth doing the lookup ahead of time and storing the ComponentInfo in the context. That avoids having to look it up in both the read_source and write_target methods. You aren't currently doing that lookup in the non-reflect versions, but you could replace the self.components.component_id::<T>() == self.component_id checks with component_info.type_id() == Some(TypeId::of::<T>()) and wind up with a little less code overall.

.unwrap()
.type_id()
.unwrap();
let component_type_id = component.reflect_type_info().type_id();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you use component.type_id() here? Any is a supertrait of Reflect, so I think it's available.

The reflect_type_info() method delegates to one on trait Typed, which is a safe trait. I believe a malicious implementation could return the wrong type, and then the copy_nonoverlapping() call would be UB. But Any is implemented by the compiler and can be relied upon.

(handler)(&mut world.into(), self);

// SAFETY: There are no other mutable references to source entity.
let Some(source_component_ptr) = (unsafe { source_entity.get_by_id(component) }) else {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The entity is guaranteed to have this component because it was part of its own archetype, right? It might be more clear to use debug_checked_unwrap() so that it's obvious get_by_id never fails.

// - component is a valid value represented by component_id
unsafe {
let raw_component =
core::ptr::NonNull::new_unchecked(Box::into_raw(component).cast::<u8>());
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does this box get deallocated somewhere? The component is moved out by insert_by_id, but I think the allocation is leaked.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is leaked according to miri, good catch.

let registry = self.type_registry?.read();
let type_id = self.components.get_info(self.component_id)?.type_id()?;
let reflect_from_ptr = registry.get_type_data::<bevy_reflect::ReflectFromPtr>(type_id)?;
// SAFETY: `source_component_ptr` stores data represented by `component_id`, which we used to get `ReflectFromPtr`.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think you need to check that reflect_from_ptr.type_id() == type_id(). A malicious client could insert the ReflectFromPtr for the wrong type by calling registry.get_mut(TypeId::of::<A>()).unwrap().insert(<ReflectFromPtr as FromType<B>>::from_type()), and then the as_reflect() call would be UB.

@eugineerd
Copy link
Contributor Author

I think this should address everything outlined by reviews at the moment. Also added a method ComponentCloneCtx::write_target_component_ptr to allow writing custom clone handlers for components without TypeId.

Copy link
Contributor

@chescock chescock left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good!

I found a safety hole that I missed on the first pass and that we should fix before merging, but the fix is simple so I'm hitting "approve" now.

crates/bevy_ecs/src/component.rs Show resolved Hide resolved
crates/bevy_ecs/src/entity/clone_entities.rs Outdated Show resolved Hide resolved
@alice-i-cecile
Copy link
Member

@eugineerd I agree with the last two suggestions; ping me when those are addressed and I'll get this merged for you!

@eugineerd
Copy link
Contributor Author

@alice-i-cecile applied suggestions and CI is green now.

@alice-i-cecile alice-i-cecile added this pull request to the merge queue Dec 18, 2024
@github-merge-queue github-merge-queue bot removed this pull request from the merge queue due to failed status checks Dec 18, 2024
@alice-i-cecile
Copy link
Member

Needs a bit of cleanup to make sure this builds okay with no_std. See https://github.com/bevyengine/bevy/actions/runs/12399422024/job/34614292140. @bushrat011899 should be able to give you a hand if you get stuck :)

@eugineerd
Copy link
Contributor Author

@alice-i-cecile fixed cargo build -p bevy_ecs --no-default-features build, probably should be fine now?

@alice-i-cecile alice-i-cecile added this pull request to the merge queue Dec 18, 2024
Merged via the queue into bevyengine:main with commit 20049d4 Dec 18, 2024
32 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
A-ECS Entities, components, systems, and events A-Reflection Runtime information about types C-Performance A change motivated by improving speed, memory usage or compile times D-Modest A "normal" level of difficulty; suitable for simple features or challenging fixes D-Unsafe Touches with unsafe code in some way S-Needs-Review Needs reviewer attention (from anyone!) to move forward
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants